package calao;

import java.util.HashMap;
import java.util.Stack;
import java.util.Vector;
import java.util.regex.Pattern;
import java.util.Map.Entry;

import calao.client.CoreEvents;
import calao.client.MemberService;
import calao.client.SessionService;
import calao.client.WorkspaceService;
import calao.storage.Field;
import calao.storage.Sequence;
import calao.storage.Storage;
import calao.storage.Table;
import calao.util.Arrays;
import calao.util.WeakMap;
import calao.util.Logger;
import static calao.util.Logger.*;

/** A Calao workspace.
 */
public class Workspace
{

	static final Pattern PATH_PATTERN = Pattern.compile("^/([\\w-\\.\\@]+/)*$");

	static final String ROOT_PATH = "/";

	static int openWorkspaceCount = 0;

	static final Table TABLE = new Table("calao.Workspace"); 

	static final Sequence ID_SEQUENCE = new Sequence("calao.WorkspaceId");

	static final int NEW_FLAG = 1;

	public static final Field<Integer> ID = 
		TABLE.createField("id", Field.INTEGER);	

	public static final Field<Integer> PARENT_ID = 
		TABLE.createField("pid", Field.INTEGER);	

	public static final Field<String> PATH = 
		TABLE.createField("path", Field.STRING);	

	public static final Field<HashMap<String,String>> PROPERTIES = 
		TABLE.createField("properties", new HashMap<String,String>());	

	public static final Field<Integer> FLAGS = 
		TABLE.createField("flags", Field.INTEGER);	

	static {
		TABLE.createPrimaryKeyIndex(ID);
		TABLE.createIndex(PATH);
		ID_SEQUENCE.setMinValue(2);
	}

	final Server server;
	final Workspace parent;
	final int id;
	final Properties properties;
	final Storage storage;
	final String path;

	private final Service.Map svcMap = new Service.Map();

	private final WeakMap<Integer,Session> sessionDir = 
		new WeakMap<Integer,Session>() {
		public Integer computeKey(Session session) {
			return session.id;
		}
	};

	private Vector<Application> appList = new Vector<Application>();

	Workspace(Server server, Workspace parent, int id, String path,
			HashMap<String,String> propertyMap, Storage storage)
			{
		this.server = server;
		this.parent = parent;
		this.id = id;
		this.path = path;
		this.properties = new Properties(
				parent!=null? parent.properties : null,
						propertyMap) 
		{
			public void save() {
				Workspace ws = Workspace.this;
				ws.server.saveProperties(ws, ws.properties.map);
			}
		};
		this.storage = storage;
		// add core services
		bind(new UserManager(), UserManager.class);
		bind(new ProfileManager(), ProfileManager.class);
		bind(new MembershipManager(), MembershipManager.class);
		bind(new SessionServiceImpl(), SessionService.class);
		bind(new MemberServiceImpl(), MemberService.class);
		bind(new WorkspaceServiceImpl(), WorkspaceService.class);
		// restore applications
		restoreApplications();
		// count this
		openWorkspaceCount++;
			}

	void initServices()
	{
		for (Service svc : svcMap.values()) svc.init();
	}

	/** Gets the workspace storage.
	 */
	public Storage getStorage()
	{
		return storage;
	}	

	void closeSessions()
	{
		for (Session session : sessionDir.entries()) session.close();
	}

	@Override
	protected void finalize()
	{
		close();
	}

	Session openSession(String alias, String password)
	{
		Stack<Workspace> path = new Stack<Workspace>();
		Workspace uws = this;
		while (uws!=null) {
			path.push(uws);
			User user = uws.getUserManager().getUser(alias, password);
			if (user!=null) while (!path.isEmpty()) {
				Membership membership = 
					path.pop().getMembershipManager().getMembership(
							user.getGlobalUserId());
				if (membership!=null && !membership.isInactive()) {
					Session session = new Session(this, alias, membership);
					sessionDir.put(session);
					return session;
				}
			}
			uws = uws.parent;
		}
		return null;
	}

	public Properties getProperties()
	{
		return properties;
	}

	/** Gets this workspace path.
	 */
	public String getPath()
	{
		return path;
	}

	/** Gets the identifier of this workspace.
	 */
	public int getId()
	{
		return id;
	}

	void forceClose()
	{
		close();
		Logger.log(INFO, "Workspace %d was forced to close", id);
	}

	private void close()
	{
		try {
			properties.save();
			storage.close();
		}
		catch (Exception e) {
			Logger.log(e);
		}
		openWorkspaceCount--;		
	}

	/** Creates a new workspace.
	 * 
	 * @param path the path of the new workspace. This path must be an extension of this 
	 * workspace path. This path must denote a valid parent workspace.
	 */
	public int createWorkspace(String path)
	{
		if (!path.startsWith(getPath())) throw new IllegalArgumentException();
		int wsid = server.newWorkspaceId();
		int parentWSID = getWorkspaceId(getParentPath(path));
		server.createWorkspace(wsid, parentWSID, path);
		return wsid;
	}

	/** Gets a child workspace.
	 * @param wsid identifier of the workspace.
	 * @return The searched workspace, or null if there is no workspace with the specified
	 * identifier, or the workspace is not a child workspace.
	 */
	public Workspace getWorkspace(int wsid)
	{
		String rpath = server.getWorkspacePath(wsid);
		if (rpath.startsWith(path)) return server.getWorkspace(wsid);
		return null;
	}

	/** Gets the identifier of a workspace identified by a given path.
	 * @param path the path of the new workspace. This path must be an extension of this 
	 * workspace path.
	 */
	public int getWorkspaceId(String path)
	{
		if (!path.startsWith(getPath())) throw new IllegalArgumentException();
		return server.getWorkspaceId(path);
	}

	/** Gets the parent path of a workspace path.
	 */
	public static String getParentPath(String path)
	{
		if (!isValidPath(path)) throw new IllegalArgumentException();
		if (path.length()==1) return null;
		int pos = path.lastIndexOf('/', path.length()-2);
		return path.substring(0, pos+1);
	}

	/** Checks whether a workspace path is valid.
	 */
	public static boolean isValidPath(String path)
	{
		return PATH_PATTERN.matcher(path).matches();
	}

	/** Deletes a workspace.
	 */
	public void deleteWorkspace(int wsid)
	{
		String path = server.getWorkspacePath(wsid);
		if (!path.startsWith(path)) throw new IllegalArgumentException();
		server.deleteWorkspace(wsid);
	}

	/** Gets the user manager.
	 */
	public UserManager getUserManager()
	{
		return getService(UserManager.class);
	}

	/** Gets the membership manager.
	 */
	public MembershipManager getMembershipManager()
	{
		return getService(MembershipManager.class);
	}

	/** Gets the profile manager.
	 */
	public ProfileManager getProfileManager()
	{
		return getService(ProfileManager.class);
	}

	/** Creates a member user.
	 */
	public User createMemberUser(String alias, String password, 
			String displayName, int[] profiles, HashMap<String,String> properties)
	{
		User user = getUserManager().createUser(alias, password);
		user.createMembership(this, displayName, profiles, properties); 
		return user;
	}

	/** Creates a member user.
	 */
	public User createMemberUser(String alias, String password, 
			String displayName, int[] profiles)
	{
		return createMemberUser(alias, password, displayName, profiles,
				new HashMap<String,String>());
	}

	/** Gets a service.
	 */
	@SuppressWarnings("unchecked")
	public synchronized <T> T getService(Class<T> face)
	{
		Service svc = svcMap.get(face.getName());
		if (svc!=null) return (T)svc;
		// first search in bound services
		for (Entry<String,Service> entry : svcMap.entrySet()) {
			if (face.isInstance(entry.getValue())) {
				svc = entry.getValue();
				svcMap.put(face.getName(), svc);
				log(DEBUG, "Aliased service %s to %s in workspace %d", 
						entry.getKey(), face.getName(), id);
				return (T)svc;
			}
		}
		// then search in bound applications
		for (Application app : appList) {
			svc = app.newService(face);
			if (svc!=null) {
				bind(svc, face);
				return (T)svc;
			}
		}
		return null;
	}
	
	/** Gets a service.
	 */
	public Service getService(String faceName)
	{
		Service svc = svcMap.get(faceName);
		if (svc==null) {
			try {
				svc = (Service)getService(Class.forName(faceName));
			}
			catch (Exception e) { }
		}
		return svc;
	}
	
	private void bind(Service service, Class serviceFace)
	{
		if (!serviceFace.isInstance(service)) {
			throw new IllegalArgumentException();
		}
		service.ws = this;
		service.init();
		svcMap.put(serviceFace.getName(), service);
		log(DEBUG, "Bound service %s to workspace %d", serviceFace.getName(), id);		
	}

	/** Adds an application to the list of applications used in this workspace.
	 * This method forces any dependency to be added as well, in dependency order.
	 * This method has no effect on applications already added.
	 */
	public boolean add(Application app)
	{
		if (!getUsedApplicationNames().contains(app.name)) {
			properties.setStringListProperty(Properties.APPLICATIONS, Arrays.append(
					properties.getStringListProperty(Properties.APPLICATIONS), 
					app.name));				
			bind(app);
			logUserEvent(CoreEvents.APPLICATION_ADDED, app.getName());
			return true;
		}
		else return false;
	}

	Application.NameSet getUsedApplicationNames()
	{
		Application.NameSet result = new Application.NameSet();
		for (String name : properties.getStringListProperty(Properties.APPLICATIONS)) {
			result.add(name);
		}
		return result;
	}

	private void bind(Application app)
	{
		if (!appList.contains(app)) {
			for (String depName : app.getDependencies()) {
				bind(server.getApplication(depName));
			}
			appList.add(app);
		}
	}
	
	/** Gets applications used in this workspace.
	 */
	public Application[] getUsedApplications()
	{
		String[] names = properties.getStringListProperty(Properties.APPLICATIONS);
		Application[] result = new Application[names.length];
		for (int i=0; i<result.length; i++) {
			result[i] = server.getApplication(names[i]);
			if (result[i]==null) throw new DependencyException(names[i]);
		}
		return result;
	}

	/** Removes an application from the list of used applications.
	 */
	public boolean remove(Application app)
	{
		for (Application uapp : appList) {
			if (uapp!=app && Arrays.contains(app.dependencies, app.name)) {
				return false;
			}
		}
		if (getUsedApplicationNames().contains(app.name)) {
			properties.setStringListProperty(Properties.APPLICATIONS, Arrays.remove(
					properties.getStringListProperty(Properties.APPLICATIONS), 
					app.name));
			restoreApplications();
			logUserEvent(CoreEvents.APPLICATION_REMOVED, app.getName());
			return true;
		}
		else return false;
	}

	void restoreApplications()
	{
		for (Application app : getUsedApplications()) bind(app);
	}
	
	Iterable<Application> getApplications()
	{
		return appList;
	}

	void logUserEvent(long guid, String eventName, Object... argv)
	{
		String[] strv = new String[argv.length];
		for (int i=0; i<argv.length; i++) strv[i] = String.valueOf(argv[i].toString());
		EventLogDB.log(storage, null, eventName, guid, strv);
		Logger.log(INFO, "User %d event %s", guid, eventName);
	}

	void logUserEvent(String eventName, Object... argv)
	{
		WebRequest req = WorkspaceServlet.getThreadLocalRequest();
		Session session = req!=null? req.getSession() : null;
		logUserEvent(session!=null? session.membership.guid : -1, eventName, argv);
	}

}
